package me.chenqiang.cinc.passportutils;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import me.chenqiang.cinc.CincCodingException;

public class PassportNumCodecs {
	private static final Logger LOGGER = LoggerFactory.getLogger(PassportNumCodecs.class);
	
	private PassportNumCodecs() {}

	private static Map<Integer, BigInteger> fibonacciFactors = new HashMap<>(16);

	private static BigInteger getFibonacciFactor(int bits) {
		if (fibonacciFactors.containsKey(bits)) {
			return fibonacciFactors.get(bits);
		} else {
			BigInteger factor = new BigDecimal(BigInteger.ONE.shiftLeft(bits + 1))
					.divide(BigDecimal.valueOf(Math.sqrt(5.0) + 1), RoundingMode.HALF_UP).toBigInteger();
			if (factor.mod(BigInteger.TWO).signum() == 0) {
				factor = factor.add(BigInteger.ONE);
			}
			fibonacciFactors.put(bits, factor);
			return factor;
		}
	}

	private static Map<Integer, BigInteger> bitOneMasks = new HashMap<>(16);

	private static BigInteger getBitOneMask(int bits) {
		if (bitOneMasks.containsKey(bits)) {
			return bitOneMasks.get(bits);
		} else {
			BigInteger mask = BigInteger.ONE.shiftLeft(bits);
			mask = mask.subtract(BigInteger.ONE);
			bitOneMasks.put(bits, mask);
			return mask;
		}
	}

	private static int getCharValue(char ch) {
		if (ch >= '0' && ch <= '9') {
			return ch - '0' + 1;
		}
		if (ch >= 'A' && ch <= 'Z') {
			return ch - 'A' + 11;
		} else {
			throw new IllegalArgumentException(String.format("Unexpected char: %c", ch));
		}
	}

	public static BigInteger fibonacciHash(String ppn, int bits) {
		BigInteger factor = getFibonacciFactor(bits);
		BigInteger mask = getBitOneMask(bits);

		BigInteger hash = BigInteger.ZERO;
		for (int i = 0; i < ppn.length(); i++) {
			hash = hash.multiply(factor).add(BigInteger.valueOf(getCharValue(ppn.charAt(i))));
		}

		return hash.and(mask);
	}

	public static BigInteger jenkinsOaatHash(String ppn, boolean fold) {
		BigInteger hash = BigInteger.ZERO;

		for (int i = 0; i < ppn.length(); i++) {
			hash = hash.add(BigInteger.valueOf(getCharValue(ppn.charAt(i))));
			hash = hash.shiftLeft(10).add(hash);
			hash = hash.shiftRight(6).xor(hash);
		}
		hash = hash.shiftLeft(3).add(hash);
		hash = hash.shiftRight(11).xor(hash);
		hash = hash.shiftLeft(15).add(hash);
		hash = hash.shiftRight(16);

		if (fold) {
			return hash.shiftRight(16).xor(hash).and(getBitOneMask(16));
		} else {
			return hash.and(getBitOneMask(32));
		}
	}

	public static BigInteger hash(String countryCode, String ppn) throws CincCodingException {
		String cleansed = ppn.replaceAll("[^0-9A-Z]", "");
		if(cleansed.length() > 9) {
			cleansed = cleansed.substring(0, 9);
		}
		String strCountryBits = CountryHuffmanCodec.encode(countryCode);
		int lenCountryBits = strCountryBits.length();
		int country = Integer.parseInt(strCountryBits, 2);
		
		if(LOGGER.isDebugEnabled()) LOGGER.debug(String.format("%s -> %s", countryCode, strCountryBits));
		
		BigInteger hash = BigInteger.valueOf(country);		
		if (lenCountryBits <= 8) {
			BigInteger fib = fibonacciHash(cleansed, 11 - lenCountryBits);
			BigInteger jks = jenkinsOaatHash(cleansed, false);			
			return hash.shiftLeft(11 - lenCountryBits).or(fib).shiftLeft(32).or(jks).shiftLeft(2);
		}
		else if(lenCountryBits <= 20) {
			BigInteger fib = fibonacciHash(cleansed, 27 - lenCountryBits);
			BigInteger jks = jenkinsOaatHash(cleansed, true);			
			return hash.shiftLeft(27 - lenCountryBits).or(fib).shiftLeft(16).or(jks).shiftLeft(2);
		}
		else {
			throw new CincCodingException(String.format("国别%s编码超过了20bit", countryCode));
		}
	}
}
